﻿package net.wangit.jdbc.executable;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;

import net.wangit.MapList;
import net.wangit.framework.config.DebugSettings;
import net.wangit.framework.debug.Recorder;
import net.wangit.jdbc.DBImplementor;
import net.wangit.jdbc.JDBCException;
import net.wangit.jdbc.QueryParameters;
import net.wangit.jdbc.config.JDBCSettings;
import net.wangit.jdbc.database.Database;
import net.wangit.jdbc.sqltype.SqlType;
import net.wangit.jdbc.sqltype.SqlTypeFactory;
import net.wangit.jdbc.util.JDBCExceptionReporter;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


public final class Query {

	private static final Log log = LogFactory
			.getLog(net.wangit.jdbc.executable.Query.class);

	public Query() {
	}

	public static final ResultSet getResultSet(DBImplementor db, String sql,
			QueryParameters queryParameters) throws SQLException, JDBCException {
		PreparedStatement st = prepareQueryStatement(db, sql, queryParameters);
		return getResultSet(st, queryParameters, db);
	}

	public static final MapList doQuery(DBImplementor db, String sql,
			QueryParameters queryParameters) throws SQLException, JDBCException {
		int maxRows;
		MapList list;
		PreparedStatement st;
		ResultSet rs;
		maxRows = getMaxRows(queryParameters);
		list = new MapList();
		st = prepareQueryStatement(db, sql, queryParameters);
		rs = getResultSet(st, queryParameters, db);
		try {
			ResultSetMetaData meta = rs.getMetaData();
			int columnCount = meta.getColumnCount();
			String names[] = new String[columnCount];
			SqlType sqlTypes[] = new SqlType[columnCount];
			for (int i = 1; i <= columnCount; i++) {
				names[i - 1] = meta.getColumnName(i).toLowerCase();
				sqlTypes[i - 1] = SqlTypeFactory.getSqlType(meta
						.getColumnType(i));
			}

			for (int count = 0; count < maxRows && rs.next(); count++) {
				for (int i = 0; i < columnCount; i++)
					list.put(count, names[i], sqlTypes[i].get(rs, names[i]));

			}

		} catch (SQLException sqle) {
			JDBCExceptionReporter.logExceptions(sqle);
			throw sqle;
		} finally {
			db.getBatcher().closeQueryStatement(st, rs);
		}
		return list;
	}

	private static final PreparedStatement prepareQueryStatement(
			DBImplementor db, String sql, QueryParameters queryParameters)
			throws SQLException, JDBCException {
		JDBCSettings js = db.getJDBCSettings();
		Database database = js.getDatabase();
		boolean useLimit = useLimit(queryParameters, database);
		boolean hasFirstRow = queryParameters.getFirstRow() > 0;
		boolean useOffset = hasFirstRow && useLimit
				&& database.supportsLimitOffset();
		boolean scrollable = js.isScrollable() && hasFirstRow && !useOffset;
		if (useLimit) {
			sql = database.getLimitString(sql, useOffset, getMaxOrLimit(
					queryParameters, database));
			if (DebugSettings.isDebugEnabled(log, db))
				log.debug((new StringBuilder("sql(use limit): ")).append(sql)
						.toString());
		}
		PreparedStatement st = db.getBatcher().prepareQueryStatement(sql,
				scrollable);
		try {
			int col = 1;
			if (useLimit && database.bindLimitParametersFirst())
				col += bindLimitParameters(st, col, queryParameters, db);
			col += bindPositionalParameters(st, queryParameters, col, db);
			if (useLimit && !database.bindLimitParametersFirst())
				col += bindLimitParameters(st, col, queryParameters, db);
			if (!useLimit)
				setMaxRows(st, queryParameters);
			if (queryParameters.getTimeout() > 0)
				st.setQueryTimeout(queryParameters.getTimeout());
		} catch (SQLException sqle) {
			JDBCExceptionReporter.logExceptions(sqle);
			db.getBatcher().closeQueryStatement(st, null);
			throw sqle;
		} catch (JDBCException he) {
			db.getBatcher().closeQueryStatement(st, null);
			throw he;
		}
		return st;
	}

	private static boolean useLimit(QueryParameters queryParameters,
			Database database) {
		return database.supportsLimit() && hasMaxRows(queryParameters);
	}

	private static boolean hasMaxRows(QueryParameters queryParameters) {
		return queryParameters.getMaxRows() > 0;
	}

	private static int getMaxRows(QueryParameters queryParameters) {
		int maxRows = queryParameters.getMaxRows();
		return maxRows <= 0 ? 0x7fffffff : maxRows;
	}

	private static int getMaxOrLimit(QueryParameters queryParameters,
			Database database) {
		int firstRow = queryParameters.getFirstRow();
		int lastRow = getMaxRows(queryParameters);
		if (database.useMaxForLimit())
			return lastRow + firstRow;
		else
			return lastRow;
	}

	private static int bindLimitParameters(PreparedStatement st, int index,
			QueryParameters queryParameters, DBImplementor db)
			throws SQLException {
		Database database = db.getJDBCSettings().getDatabase();
		if (!database.supportsVariableLimit())
			return 0;
		if (!hasMaxRows(queryParameters))
			throw new RuntimeException("Bug binding LIMIT parameters");
		int firstRow = queryParameters.getFirstRow();
		int lastRow = getMaxOrLimit(queryParameters, database);
		boolean hasFirstRow = firstRow > 0 && database.supportsLimitOffset();
		boolean reverse = database.bindLimitParametersInReverseOrder();
		if (hasFirstRow)
			st.setInt(index + (reverse ? 1 : 0), firstRow);
		st.setInt(index + (!reverse && hasFirstRow ? 1 : 0), lastRow);
		return hasFirstRow ? 2 : 1;
	}

	private static int bindPositionalParameters(PreparedStatement st,
			QueryParameters queryParameters, int start, DBImplementor db)
			throws SQLException, JDBCException {
		String values[] = queryParameters.getValues();
		int types[] = queryParameters.getTypes();
		if (values == null || types == null)
			return 0;
		if (values.length != types.length)
			throw new RuntimeException(
					"Bug with the QueryParameters's types or values");
		Database database = db.getJDBCSettings().getDatabase();
		int span = 0;
		for (int i = 0; i < values.length; i++) {
			SqlType sqlType = database.getSqlType(types[i]);
			sqlType.set(st, values[i], start + span);
			span++;
		}

		return span;
	}

	private static void setMaxRows(PreparedStatement st,
			QueryParameters queryParameters) throws SQLException {
		if (hasMaxRows(queryParameters))
			st.setMaxRows(getMaxRows(queryParameters)
					+ queryParameters.getFirstRow());
	}

	private static final ResultSet getResultSet(PreparedStatement st,
			QueryParameters queryParameters, DBImplementor db)
			throws SQLException, JDBCException {
		ResultSet rs;
		Recorder.countQuery();
		rs = null;
		try {
			rs = db.getBatcher().getResultSet(st);
			Database database = db.getJDBCSettings().getDatabase();
			if (!database.supportsLimitOffset()
					|| !useLimit(queryParameters, database))
				advance(rs, queryParameters, db);
			return rs;
		} catch (SQLException sqle) {
			JDBCExceptionReporter.logExceptions(sqle);
			db.getBatcher().closeQueryStatement(st, rs);
			throw sqle;
		}
	}

	private static void advance(ResultSet rs, QueryParameters queryParameters,
			DBImplementor db) throws SQLException {
		int firstRow = queryParameters.getFirstRow();
		if (firstRow != 0)
			if (db.getJDBCSettings().isScrollable()) {
				rs.absolute(firstRow);
			} else {
				for (int m = 0; m < firstRow; m++)
					rs.next();

			}
	}

}
